import {
  Shader,
  ShaderData,
  ShaderProperty,
  ShaderPool,
  ShaderProgram,
  ShaderProgramPool,
  ShaderMacro,
  ShaderMacroCollection,
  ShaderDataGroup,
  ShaderLib
} from "@nirvana/shaders";
import { Vector3, Matrix4, Color } from "@nirvana/math";

import { WebGLCanvas } from "@nirvana/canvas";
import { VertexBufferData, IndexBufferData, DataElement } from "@nirvana/core";
import { parseOBJ, parseMTL } from "./obj.loader";

async function main() {
  const shaderMacroCollection: ShaderMacroCollection = new ShaderMacroCollection();
  console.log(shaderMacroCollection);
  const colorShaderData: ShaderData = new ShaderData(ShaderDataGroup.Material);
  console.log(ShaderDataGroup, colorShaderData);
  const pool: ShaderProgramPool = new ShaderProgramPool();

  // 使用使用 Canvas
  let canvas = document.getElementById("engine-nirvana");
  const webCanvas = new WebGLCanvas(canvas, {
    rate: 1
  });
  const engine = webCanvas.engine;
  const gl = engine.gl;

  function isPowerOf2(value) {
    return (value & (value - 1)) === 0;
  }

  function create1PixelTexture(gl, pixel) {
    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(pixel));
    return texture;
  }

  function createTexture(gl, url) {
    const texture = create1PixelTexture(gl, [128, 192, 255, 255]);
    // Asynchronously load an image
    const image = new Image();
    image.src = url;
    image.addEventListener("load", function () {
      // Now that the image has loaded make copy it to the texture.
      gl.bindTexture(gl.TEXTURE_2D, texture);
      gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

      // Check if the image is a power of 2 in both dimensions.
      if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
        // Yes, it's a power of 2. Generate mips.
        gl.generateMipmap(gl.TEXTURE_2D);
      } else {
        // No, it's not a power of 2. Turn of mips and set wrapping to clamp to edge
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
      }
    });
    return texture;
  }

  const cVS = `
  attribute vec4 a_position;
  attribute vec3 a_normal;
  attribute vec2 a_texcoord;
  // attribute vec4 a_color;

  uniform mat4 u_projection;
  uniform mat4 u_view;
  uniform mat4 u_world;

  uniform mat4 rotateMatrix;
  uniform mat4 scaleMatrix;
  uniform vec3 u_viewWorldPosition;

  varying vec3 v_normal;
  varying vec3 v_surfaceToView;
  varying vec2 v_texcoord;
  varying vec4 v_color;

  void main() {
    gl_Position = rotateMatrix * scaleMatrix * a_position;
    v_surfaceToView = u_viewWorldPosition - a_position.xyz;
    v_normal = mat3(rotateMatrix) * a_normal;
    v_texcoord = a_texcoord;
    v_color = vec4(1.0,1.0,1.0,1.0);
  }
  `;
  const cFS = `
  precision mediump float;

  varying vec3 v_normal;
  varying vec3 v_surfaceToView;
  varying vec2 v_texcoord;
  varying vec4 v_color;

  uniform vec3 diffuse;
  uniform sampler2D diffuseMap;
  uniform vec3 ambient;
  uniform vec3 emissive;
  uniform vec3 specular;
  uniform sampler2D specularMap;
  uniform float shininess;
  uniform float opacity;
  uniform vec3 u_lightDirection;
  uniform vec3 u_ambientLight;

  void main () {
    vec3 normal = normalize(v_normal);

    vec3 surfaceToViewDirection = normalize(v_surfaceToView);
    vec3 halfVector = normalize(u_lightDirection + surfaceToViewDirection);

    float fakeLight = dot(u_lightDirection, normal) * .5 + .5;
    float specularLight = clamp(dot(normal, halfVector), 0.0, 1.0);
    vec4 specularMapColor = texture2D(specularMap, v_texcoord);
    vec3 effectiveSpecular = specular * specularMapColor.rgb;

    vec4 diffuseMapColor = texture2D(diffuseMap, v_texcoord);
    vec3 effectiveDiffuse = diffuse * diffuseMapColor.rgb * v_color.rgb;
    float effectiveOpacity = opacity * diffuseMapColor.a * v_color.a;

    gl_FragColor = vec4(
        emissive +
        ambient * u_ambientLight +
        effectiveDiffuse * fakeLight +
        effectiveSpecular * pow(specularLight, shininess),
        effectiveOpacity);
  }`;
  const colorDemo = Shader.create("colorDemo", cVS, cFS);
  console.log(colorDemo, Shader.find("colorDemo"));

  const colorProgram: ShaderProgram = colorDemo.getShaderProgram(engine.gl, shaderMacroCollection, pool);
  console.log("color", colorProgram);

  // const objHref = "/dev-static/obj/windmill/windmill.obj";
  // const objHref = "/dev-static/obj/book/book.obj";
  const objHref = "/dev-static/obj/chair/chair.obj";
  // const objHref = "/dev-static/obj/Squirtle/Squirtle.obj";
  // const objHref = "/dev-static/obj/pikaqiu/pikaqiu.obj";
  const response = await fetch(objHref);
  const text = await response.text();
  const obj = parseOBJ(text);
  console.log(obj);
  const baseHref = new URL(objHref, window.location.href);
  const matTexts = await Promise.all(
    obj.materialLibs.map(async (filename) => {
      const matHref = new URL(filename, baseHref).href;
      const response = await fetch(matHref);
      return await response.text();
    })
  );
  const materials = parseMTL(matTexts.join("\n"));
  const textures = {
    defaultWhite: create1PixelTexture(gl, [255, 255, 255, 255])
  };

  // load texture for materials
  for (const name in materials) {
    const material = materials[name];
    Object.entries(material)
      .filter(([key]) => key.endsWith("Map"))
      .forEach(([key, filename]: [any, any]) => {
        let texture = textures[filename];
        if (!texture) {
          const textureHref = new URL(filename, baseHref).href;
          texture = createTexture(gl, textureHref);
          textures[filename] = texture;
        }
        material[key] = texture;
      });
  }
  console.log(materials);
  // console.log(materials);
  // hack the materials so we can see the specular map
  Object.values(materials).forEach((m: any) => {
    m.shininess = 25;
    m.specular = [3, 2, 1];
  });

  const defaultMaterial = {
    diffuse: [1, 1, 1],
    diffuseMap: textures.defaultWhite,
    ambient: [0, 0, 0],
    specular: [1, 1, 1],
    specularMap: textures.defaultWhite,
    shininess: 400,
    opacity: 1
  };

  const parts = obj.geometries.map(({ material, data }) => {
    // Because data is just named arrays like this
    //
    // {
    //   position: [...],
    //   texcoord: [...],
    //   normal: [...],
    // }
    //
    // and because those names match the attributes in our vertex
    // shader we can pass it directly into `createBufferInfoFromArrays`
    // from the article "less code more fun".

    // create a buffer for each array by calling
    // gl.createBuffer, gl.bindBuffer, gl.bufferData
    if (data.color) {
      if (data.position.length === data.color.length) {
        // it's 3. The our helper library assumes 4 so we need
        // to tell it there are only 3.
        data.color = { numComponents: 3, data: data.color };
      }
    } else {
      // there are no vertex colors so just use constant white
      data.color = [1, 1, 1, 1];
    }

    const pBuffer = new VertexBufferData(new Float32Array(data.position), [
      new DataElement("a_position", 3, gl.FLOAT, false, 0, 0)
    ]);

    const nBuffer = new VertexBufferData(new Float32Array(data.normal), [
      new DataElement("a_normal", 3, gl.FLOAT, false, 0, 0)
    ]);

    const tBuffer = new VertexBufferData(new Float32Array(data.texcoord), [
      new DataElement("a_texcoord", 2, gl.FLOAT, false, 0, 0)
    ]);

    // const cBuffer = new VertexBufferData(new Float32Array(data.color), [
    //   new DataElement("a_color", 4, gl.FLOAT, false, 0, 0)
    // ]);

    pBuffer.create(gl);
    nBuffer.create(gl);
    tBuffer.create(gl);
    // cBuffer.create(gl);
    return {
      material: {
        ...defaultMaterial,
        ...materials[material]
      },
      u_diffuse: new Color(Math.random(), Math.random(), Math.random(), 1),
      pBuffer,
      nBuffer,
      tBuffer
      // cBuffer
    };
  });

  console.log(parts);

  function getExtents(positions) {
    const min = positions.slice(0, 3);
    const max = positions.slice(0, 3);
    for (let i = 3; i < positions.length; i += 3) {
      for (let j = 0; j < 3; ++j) {
        const v = positions[i + j];
        min[j] = Math.min(v, min[j]);
        max[j] = Math.max(v, max[j]);
      }
    }
    return { min, max };
  }

  function getGeometriesExtents(geometries) {
    return geometries.reduce(
      ({ min, max }, { data }) => {
        const minMax = getExtents(data.position);
        return {
          min: min.map((min, ndx) => Math.min(minMax.min[ndx], min)),
          max: max.map((max, ndx) => Math.max(minMax.max[ndx], max))
        };
      },
      {
        min: Array(3).fill(Number.POSITIVE_INFINITY),
        max: Array(3).fill(Number.NEGATIVE_INFINITY)
      }
    );
  }

  // const extents = getGeometriesExtents(obj.geometries);
  // const range = m4.subtractVectors(extents.max, extents.min);
  // // amount to move the object so its center is at the origin
  // const objOffset = m4.scaleVector(
  //     m4.addVectors(
  //       extents.min,
  //       m4.scaleVector(range, 0.5)),
  //     -1);
  const cameraTarget = [0, 0, 0];

  let w = window.innerWidth;
  let h = window.innerHeight;
  let srate = w / h;

  const _scale = Matrix4.e.scaling(new Vector3(0.1, 0.1 * srate, 0.1));
  const _trans = Matrix4.e.translation(new Vector3(0, -1, 0));
  const rotateMatrix = _scale.clone(); //.leftDot(_trans);
  console.log(_scale, _trans);
  // { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }

  let r = 0;
  const e = Matrix4.e;
  const axis = new Vector3(0.0, 1.0, 0.0);
  console.log(rotateMatrix);
  function cv3(arr) {
    if (arr && arr.length > 0) {
      return new Vector3(arr[0], arr[1], arr[2]);
    }
    return new Vector3(0, 0, 0);
  }
  engine.update = (gl) => {
    // console.log('--');
    gl.clearColor(1.0, 1.0, 1.0, 1.0);
    // 用上面指定的颜色清除缓冲区
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.enable(gl.DEPTH_TEST);
    // gl.enable(gl.CULL_FACE);
    r += 0.01;
    rotateMatrix.rotationAxisAngle(axis, r);
    // 颜色 start 三
    const u_lightDirection = new Vector3(-1, 3, 5).normalize();
    parts.map((part) => {
      colorProgram.bind();

      part.pBuffer.upload(colorProgram._glProgram, gl);
      part.nBuffer.upload(colorProgram._glProgram, gl);
      part.tBuffer.upload(colorProgram._glProgram, gl);
      // part.cBuffer.upload(colorProgram._glProgram, gl);
      const shaderData: ShaderData = new ShaderData(ShaderDataGroup.Material);
      // console.log(ShaderDataGroup, shaderData);
      shaderData.setMatrix("rotateMatrix", rotateMatrix);
      shaderData.setMatrix("scaleMatrix", _scale);
      shaderData.setMatrix("u_projection", e);
      shaderData.setMatrix("u_view", e);
      shaderData.setMatrix("u_world", e);
      // shaderData.setVector4("u_diffuse", part.u_diffuse);
      shaderData.setVector3("u_lightDirection", u_lightDirection);
      shaderData.setVector3("u_viewWorldPosition", new Vector3(0, 0, 200.0));
      shaderData.setVector3("diffuse", cv3(part.material.diffuse));
      shaderData.setVector3("ambient", cv3(part.material.ambient));
      shaderData.setVector3("emissive", cv3(part.material.emissive));
      shaderData.setVector3("specular", cv3(part.material.specular));
      shaderData.setVector3("u_ambientLight", new Vector3(0.2, 0.2, 0.2));
      shaderData.setTexture("diffuseMap", { _glTexture: part.material.diffuseMap, _target: gl.TEXTURE_2D });
      shaderData.setTexture("specularMap", { _glTexture: part.material.specularMap, _target: gl.TEXTURE_2D });
      shaderData.setFloat("shininess", part.material.shininess);
      shaderData.setFloat("opacity", part.material.opacity);
      colorProgram.uploadAll(colorProgram.otherUniformBlock, shaderData);
      part.pBuffer.draw(gl);
    });
    // vBuffer.upload(colorProgram._glProgram, gl);
    // iBuffer.upload(gl);
    // iBuffer.draw(gl);
  };
}

main();
